# 2.渲染进程中的 Event Loop(事件循环机制)
选自: 带你彻底弄懂 Event Loop (opens new window)
什么是 Event Loop? (opens new window)
# 宏队列和微队列
# 为什么消息队列要有宏队列和微队列之分
- 宏任务可以满足我们大部分的日常需求,但是宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,
# 宏队列,macrotask,也叫 tasks。
一些异步任务的回调会依次进入 macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node 独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
# 微队列,microtask,也叫 jobs。
另一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node 独有)
- Promise
- Object.observe
- MutationObserver(用来监视 DOM 变动。比如节点的增减、属性的变动、文本内容的变动。) (注:这里只针对浏览器和 NodeJS)
# 浏览器的 Event Loop
我们先来看一张图,再看完这篇文章后,请返回来再仔细看一下这张图,相信你会有更深的理解。
这张图将浏览器的 Event Loop 完整的描述了出来,我来讲执行一个 JavaScript 代码的具体流程:
- 执行全局 Script 同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如 setTimeout 等);
- 全局 Script 代码执行完毕后,调用栈 Stack 会清空;
- 从微队列 microtask queue 中取出位于队首的回调任务,放入调用栈 Stack 中执行,执行完后 microtask queue 长度减 1;
- 继续取出位于队首的任务,放入调用栈 Stack 中执行,以此类推,直到直到把 microtask queue 中的所有任务都执行完毕。
- 注意,如果在执行 microtask 的过程中,又产生了 microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue 中的所有任务都执行完毕,此时 microtask queue 为空队列,调用栈 Stack 也为空;
- 取出宏队列 macrotask queue 中位于队首的任务,放入 Stack 中执行; 执行完毕后,调用栈 Stack 为空;
- 重复第 3-7 个步骤;
- 重复第 3-7 个步骤; ...... 可以看到,这就是浏览器的事件循环 Event Loop
这里归纳 3 个重点:
- 宏队列 macrotask 一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 微任务队列中所有的任务都会被依次取出来执行,直到 microtask queue 为空;
- 图中没有画 UI rendering 的节点,因为这个是由浏览器自行判断决定的,但是只要执行 UI rendering,它的节点是在执行完所有的 microtask 之后,下一个 macrotask 之前,紧跟着执行 UI render。
- 所以导致一个问题:微任务,如果添加速度大于执行速度,并且一直添加,则微队列一直不为空,它就会阻塞 UI 线程
- 好了,概念性的东西就这么多,来看几个示例代码,测试一下你是否掌握了:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
});
new Promise((resolve, reject) => {
console.log(4);
resolve(5);
}).then((data) => {
console.log(data);
});
setTimeout(() => {
console.log(6);
});
console.log(7);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 正确答案
1;
4;
7;
5;
2;
3;
6;
2
3
4
5
6
7
8
我们来分析一下整个流程:
Step 1
console.log(1);
Stack Queue: [console]
Macrotask Queue: []
Microtask Queue: []
打印结果: 1
Step 2
setTimeout(() => {
// 这个回调函数叫做callback1,setTimeout属于macrotask,所以放到macrotask queue中
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
});
2
3
4
5
6
7
Stack Queue: [setTimeout]
Macrotask Queue: [callback1]
Microtask Queue: []
打印结果: 1
Step 3
new Promise((resolve, reject) => {
// 注意,这里是同步执行的
console.log(4);
resolve(5);
}).then((data) => {
// 这个回调函数叫做callback2,promise属于microtask,所以放到microtask queue中
console.log(data);
});
2
3
4
5
6
7
8
Stack Queue: [promise]
Macrotask Queue: [callback1] //宏队列
Microtask Queue: [callback2] //微队列
打印结果: 1 4
Step 4
setTimeout(() => {
// 这个回调函数叫做callback3,setTimeout属于macrotask,所以放到macrotask queue中
console.log(6);
});
2
3
4
Stack Queue: [setTimeout]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
打印结果: 1 4
Step 5 console.log(7)
打印结果: 1 4 7
- 好啦,全局 Script 代码执行完了,进入下一个步骤,从 microtask queue 中依次取出任务执行,直到 microtask queue 队列为空。
Stack Queue: [console]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
Step 6 console.log(data) // 这里 data 是 Promise 的决议值 5
Stack Queue: [callback2]
Macrotask Queue: [callback1, callback3]
Microtask Queue: []
打印结果:
1
4
7
5
2
3
4
5
- 这里 microtask queue 中只有一个任务,执行完后开始从宏任务队列 macrotask queue 中取位于队首的任务执行
Step 7
console.log(2);
Stack Queue: [callback1]
Macrotask Queue: [callback3]
Microtask Queue: []
- 但是,执行 callback1 的时候又遇到了另一个 Promise,Promise 异步执行完后在 microtask queue 中又注册了一个 callback4 回调函数 Step 8
Promise.resolve().then(() => {
// 这个回调函数叫做callback4,promise属于microtask,所以放到microtask queue中
console.log(3);
});
2
3
4
Stack Queue: [promise]
Macrotask v: [callback3]
Microtask Queue: [callback4]
- 取出一个宏任务 macrotask 执行完毕,然后再去微任务队列 microtask queue 中依次取出执行
Step 9
console.log(3);
Stack Queue: [callback4]
Macrotask Queue: [callback3]
Microtask Queue: []
微任务队列全部执行完,再去宏任务队列中取第一个任务执行
以上,全部执行完后,Stack Queue 为空,Macrotask Queue 为空,Micro Queue 为空
再来一个例子:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3);
});
});
new Promise((resolve, reject) => {
console.log(4);
resolve(5);
}).then((data) => {
console.log(data);
Promise.resolve()
.then(() => {
console.log(6);
})
.then(() => {
console.log(7);
setTimeout(() => {
console.log(8);
}, 0);
});
});
setTimeout(() => {
console.log(9);
});
console.log(10);
// 正确答案
1;
4;
10;
5;
6;
7;
2;
3;
9;
8;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
在执行微队列 microtask queue 中任务的时候,如果又产生了 microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到 microtask queue 为空停止。
注:当然如果你在 microtask 中不断的产生 microtask,那么其他宏任务 macrotask 就无法执行了,但是这个操作也不是无限的,拿 NodeJS 中的微任务 process.nextTick()来说,它的上限是 1000 个
# 进阶题目:
//2 3 5 6 4 7 8 1
setTimeout(() => {
console.log(1);
}, 0);
console.log(2);
async1();
async function async1() {
console.log(3);
await async2();
console.log(4);
//对await进行改写
// new Promise((resolve)=>{
// async2()
// resolve()
// }).then(()=>{
// console.log(4);
// })
}
requestAnimationFrame(() => {
console.log(8);
});
async function async2() {
console.log(5);
}
new Promise((resolve, reject) => {
console.log(6);
resolve();
}).then(() => {
console.log(7);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 start");
return new Promise((resolve, reject) => {
resolve();
console.log("async2 promise");
});
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
})
.then(function () {
console.log("promise2");
})
.then(function () {
console.log("promise3");
});
console.log("script end");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
new Promise((resolve, reject) => {
console.log("async1 start");
console.log("async2");
resolve(Promise.resolve());
}).then(() => {
console.log("async1 end");
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
})
.then(function () {
console.log("promise2");
})
.then(function () {
console.log("promise3");
})
.then(function () {
console.log("promise4");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21